import path from "path";
import * as fs from 'fs';
import { IBuildResult, IBuildTaskOption, IBundleConfig, IOutputSettings, Platform } from "../../../@types/packages/builder/@types";
import ts from "typescript";
import { AssetInfo } from "../../../@types/packages/asset-db/@types/public";

const settingsFileReg = /^settings\.(\w+\.)?json$/g;
const jsRegisterReg = /^\s*System.register\(\s*[\"\'](.*?)[\"\']\s*,\s*\[\s*(.*?)\]\s*,/;
const jsChunkNameReg = /^chunks:\/\/\/_virtual\/(.*)$/;
const ignoreJsDepends = [
    'cc',
    'cc/env',
    './rollupPluginModLoBabelHelpers.js',
    './cjs-loader.mjs',
    './minimal.js',
];

function deleteDirIfExist(dir: string): void {
    if (fs.existsSync(dir)) {
        fs.rmSync(dir, { recursive: true, force: true });
        console.log(`removed dir: ${dir} as it's not been depended`)
    }
}

function analyzeByAST(file: string) {
    // ========== 创建 AST ==========
    const sourceFile = ts.createSourceFile(
        "temp.ts",
        fs.readFileSync(file, 'utf-8'),
        ts.ScriptTarget.Latest
    );

    // ========== AST 遍历 ==========
    let result: Set<string> = new Set();

    ts.forEachChild(sourceFile, (node) => {
        if (!ts.isClassDeclaration(node)) return;

        // ========== 装饰器解析 ==========
        const decorators = ts.getDecorators(node) || [];

        decorators.forEach(decorator => {
            const expr = decorator.expression;
            if (!ts.isCallExpression(expr)) return;

            // 解析装饰器名称
            const decoratorName = expr.expression.getText(sourceFile);

            // 提取参数内容
            const args = expr.arguments
                .map(arg => eval(arg.getText(sourceFile)));

            if (decoratorName === 'registerScriptUI' && args.length > 1 && args[1]) {
                result.add(args[1])
            }
        });
    });

    return result;
}

async function collectScriptUIReferences(): Promise<Map<string, Set<string>>> {
    // script ui
    const viewFolderUrl = "db://assets/app-bundle/app-view";
    const assets: AssetInfo[] = await Editor.Message.request('asset-db', 'query-assets', { pattern: viewFolderUrl + '/**/*.ts' });
    const result = new Map<string, Set<string>>();
    for (const asset of assets) {
        const bundles = analyzeByAST(asset.file);
        if (bundles.size > 0) {
            result.set(asset.uuid, bundles);
        }
    }

    return result;
}

async function collectScriptBundleDependencies(buildResult: IBuildResult): Promise<Map<string, Set<string>>> {
    const scriptUIs = await collectScriptUIReferences();
    const result = new Map<string, Set<string>>();
    for (const [uuid, deps] of scriptUIs) {
        if (!buildResult.containsAsset(uuid)) continue;
        const infos = buildResult.getAssetPathInfo(uuid);
        for (const info of infos) {
            if (!info.bundleName) continue;
            if (!result.has(info.bundleName)) {
                result.set(info.bundleName, deps);
            } else {
                for (const dep of deps) {
                    result.get(info.bundleName).add(dep);
                }
            }
        }
    }
    return result;
}

function excludeBundle(name: string, settings: IOutputSettings, dir: string): void {
    let projectBundles = settings.assets.projectBundles;
    projectBundles.splice(projectBundles.indexOf(name), 1);
    let remotes = settings.assets.remoteBundles;
    if (remotes.indexOf(name) !== -1) {
        remotes.splice(remotes.indexOf(name), 1);
        deleteDirIfExist(path.join(dir, "remote", name));
        deleteDirIfExist(path.join(dir, "src", "bundle-scripts", name));
    }
    let subpackages = settings.assets.subpackages;
    if (subpackages.indexOf(name) !== -1) {
        subpackages.splice(subpackages.indexOf(name), 1);
        deleteDirIfExist(path.join(dir, "subpackages", name));
    }
    deleteDirIfExist(path.join(dir, "assets", name));
    if (settings.assets.bundleVers[name]) {
        delete settings.assets.bundleVers[name];
    }
}

function findSettingFile(srcPath: string): string {
    const files = fs.readdirSync(srcPath);
    for (const file of files) {
        if (settingsFileReg.test(file))
            return path.join(srcPath, file);
    }

    throw new Error("Cannot find settings.json in dir:" + srcPath);
}

function traverseDirectoryRecursive(dir: string, callback: (dir: string, name: string) => void): void {
    fs.readdirSync(dir).forEach((file) => {
        const filePath = path.join(dir, file);
        const stats = fs.statSync(filePath);
        if (stats.isDirectory()) {
            callback(filePath, file);
            traverseDirectoryRecursive(filePath, callback); // 递归调用
        }
    });
}

function extendDeepDenps(self: string, set: Set<string>, deps: Set<string>, allDeps: Map<string, Set<string>>): boolean {
    for (const dep of deps) {
        if (self === dep)
            return true;
        set.add(dep);
        if (!allDeps.has(dep)) continue;
        if (extendDeepDenps(self, set, allDeps.get(dep), allDeps)) {
            return true;
        }
    }

    return false;
}

function extendAndCheckCircleDependent(allBundels: string[], deps: Map<string, Set<string>>): boolean {
    for (const name of allBundels) {
        if (!deps.has(name)) continue;
        let set = new Set<string>();
        if (extendDeepDenps(name, set, deps.get(name), deps)) {
            return true;
        }
        deps.set(name, set);
    }

    return false;
}

function isScriptMoved(options: IBuildTaskOption): boolean {
    if (options.moveRemoteBundleScript)
        return true
    switch (options.platform as string) {
        case 'wechatgame':
        case 'bytedance-mini-game':
        case 'alipay-mini-game':
        case 'oppo-mini-game':
        case 'vivo-mini-game':
        case 'huawei-quick-game':
        case 'xiaomi-quick-game':
        case 'migu-mini-game':
        case 'taobao-mini-game':
            return true;
        default:
            return false;
    }
}

function getDestPath(srcPath: string, platform: Platform): string {
    switch (platform as string) {
        case 'android':
        case 'ohos':
        case 'open-harmonyos':
        case 'ios':
        case 'mac':
        case 'harmonyos-next':
        case 'google-play':
            return path.join(srcPath, 'data');
        case 'vivo-mini-game':
            return path.join(srcPath, 'src');
        default:
            break;
    }
    return srcPath;
}

function getSubpackageJsonPath(name: string, platform: Platform): string {
    switch (platform) {
        case 'huawei-quick-game':
        case 'vivo-mini-game':
        case 'oppo-mini-game':
            return path.join(`usr_${name}`);
        default:
            return path.join('subpackages', name);
    }
}

function getSubpackageJsFile(name: string, platform: Platform): string {
    switch (platform) {
        case 'huawei-quick-game':
        case 'vivo-mini-game':
            return path.join(`usr_${name}`, 'game.js');
        case 'oppo-mini-game':
            return path.join(`usr_${name}`, 'main.js');
        case 'xiaomi-quick-game':
            return path.join('subpackages', name, 'main.js');
        default:
            return path.join('subpackages', name, 'game.js');
    }
}

function getConfigFilePrefix(platform: Platform): string {
    switch (platform as string) {
        case 'android':
        case 'ohos':
        case 'open-harmonyos':
        case 'ios':
        case 'mac':
        case 'harmonyos-next':
        case 'google-play':
            return 'cc.';
        default:
            return '';
    }
}

export async function resaveAllBundleDependencies(buildResult: IBuildResult, options: IBuildTaskOption): Promise<void> {
    let dstPath: string = buildResult.dest;
    const scriptMoved = isScriptMoved(options);
    const platform = options.platform;
    console.log(`resaveAllBundleDependencies ${platform}: ${dstPath} ${scriptMoved}`);
    dstPath = getDestPath(dstPath, platform);
    const file = findSettingFile(path.join(dstPath, 'src'));
    const settings: IOutputSettings = JSON.parse(fs.readFileSync(file, 'utf-8'));
    const allBundles = settings.assets.projectBundles;
    const remoteBundles = settings.assets.remoteBundles;
    const bundleVers = settings.assets.bundleVers;
    const subpackages = settings.assets.subpackages;
    const configFilePrefix = getConfigFilePrefix(platform);
    const md5Bundles = Object.keys(bundleVers);
    let deps: Map<string, Set<string>> = new Map();
    let jsBundles: Map<string, string> = new Map();
    let bundleImports: Map<string, Set<string>> = new Map();
    let autoExcludeBundles: string[] = [];
    let cantBeDepBundles: string[] = [];
    let excludeDeps: Map<string, Set<string>> = new Map();
    let cantDepOthersBundles: string[] = [];
    let extDeps: Map<string, Set<string>> = new Map();
    let releaseActions: { [key: string]: number } = {};
    for (const name of allBundles) {
        const bundleVersion = md5Bundles.indexOf(name) !== -1 ? bundleVers[name] : undefined;
        const isRemote = remoteBundles.indexOf(name) !== -1;
        const isSubpackage = subpackages.indexOf(name) !== -1;
        let jsonFile: string;
        let jsFile: string;
        if (isSubpackage) {
            jsFile = path.join(dstPath, getSubpackageJsFile(name, platform));
            jsonFile = path.join(dstPath
                , getSubpackageJsonPath(name, platform)
                , bundleVersion ? `${configFilePrefix}config.${bundleVersion}.json` : `${configFilePrefix}config.json`
            );
        } else {
            jsFile = path.join(dstPath
                , isRemote ? (scriptMoved ? 'src/bundle-scripts' : 'remote') : 'assets'
                , name
                , bundleVersion ? `index.${bundleVersion}.js` : 'index.js'
            );
            jsonFile = path.join(dstPath
                , isRemote ? 'remote' : 'assets'
                , name
                , bundleVersion ? `${configFilePrefix}config.${bundleVersion}.json` : `${configFilePrefix}config.json`
            );
        }
        const config: IBundleConfig = JSON.parse(fs.readFileSync(jsonFile, 'utf-8'));
        deps.set(name, new Set(config.deps));
        const jsContent = fs.readFileSync(jsFile, 'utf-8');
        const lines = jsContent.split(/\r?\n/);
        let allImports = new Set<string>();
        for (const line of lines) {
            const match = line.match(jsRegisterReg);
            if (!match) continue;
            const chunkName = match[1];
            const imports = match[2];
            const chunkMatch = chunkName.match(jsChunkNameReg);
            if (!chunkMatch) {
                throw new Error(`Unknow register name with ${chunkName} in file ${jsFile}`);
            }
            const fileName = chunkMatch[1];
            if (fileName != name) {
                jsBundles.set('./' + fileName, name);
            }
            for (const str of imports.split(',')) {
                const dep = eval(str);
                if (dep && ignoreJsDepends.indexOf(dep) === -1) {
                    allImports.add(dep);
                }
            }
        }
        bundleImports.set(name, allImports);
    }

    // 查找所有bundle的meta文件以添加扩展依赖
    traverseDirectoryRecursive(path.join(Editor.Project.path, "assets"), (dir: string, name: string) => {
        const mataPath = dir + ".meta";
        if (!fs.existsSync(mataPath)) return;
        const meta = JSON.parse(fs.readFileSync(mataPath, 'utf-8'));
        const userData = meta['userData']
        if (!userData || !userData['isBundle']) return;
        const bundleName = userData['bundleName'] ?? name;
        if (allBundles.indexOf(bundleName) === -1) return;
        if (userData['auto_exclude']) {
            autoExcludeBundles.push(bundleName);
        }
        if (userData['cannot_be_dep']) {
            cantBeDepBundles.push(bundleName);
        }
        if (userData['cannot_dep_others']) {
            cantDepOthersBundles.push(bundleName);
        }
        if (userData['release_action']) {
            releaseActions[bundleName] = userData['release_action'];
        }
        const excludes: string = userData['exclude_deps'];
        if (excludes) {
            for (const n of excludes.split(',')) {
                const fixName = n.trim();
                if (fixName.length != 0) {
                    if (!excludeDeps.has(bundleName)) {
                        excludeDeps.set(bundleName, new Set<string>());
                    }
                    excludeDeps.get(bundleName).add(fixName);
                }
            }
        }
        const exts: string = userData['dep_ext'];
        if (exts) {
            for (const n of exts.split(',')) {
                const fixName = n.trim();
                if (fixName.length != 0) {
                    if (!deps.has(bundleName)) {
                        deps.set(bundleName, new Set<string>());
                    }
                    if (!extDeps.has(bundleName)) {
                        extDeps.set(bundleName, new Set<string>());
                    }
                    deps.get(bundleName).add(fixName);
                    extDeps.get(bundleName).add(fixName);
                }
            }
        }
    });

    // 合并js依赖
    for (const name of allBundles) {
        if (!bundleImports.has(name)) continue;
        for (const js of bundleImports.get(name)) {
            if (!jsBundles.has(js)) {
                console.warn(`Cannot find which bundle is script:${js} in! (${name})`);
                continue;
            }
            const bundle = jsBundles.get(js);
            if (bundle == name) continue;
            if (!deps.has(name)) {
                deps.set(name, new Set<string>());
            }
            deps.get(name).add(bundle);
        }
    }

    // 检查不允许依赖外部的Bundle是否有外部依赖
    for (const bn of cantDepOthersBundles) {
        if (deps.has(bn)) {
            for (const dep of deps.get(bn)) {
                if (!extDeps.has(bn) || !extDeps.get(bn).has(dep)) {
                    throw new Error(`${bn} cannot be depend other bundlers, but depend on ${[...deps.get(bn)].join(',')}`);
                }
            }
        }
    }

    // 检查不允许被依赖的Bundle是否被引用
    for (const bn of cantBeDepBundles) {
        for (const kv of deps) {
            if (kv[1].has(bn)) {
                const enableDeps = excludeDeps.has(bn) ? excludeDeps.get(bn) : new Set();
                if (enableDeps.has(kv[0])) {
                    continue;
                }
                throw new Error(`${bn} can only be depend ${[...enableDeps].join(',')}  but ${kv[0]} depend on it!`)
            }
        }
    }

    if (autoExcludeBundles.length > 0) {
        const scriptBundleDeps = await collectScriptBundleDependencies(buildResult);
        while (autoExcludeBundles.length > 0) {
            let allDeps: Map<string, Set<string>> = new Map();
            for (const kv of deps) {
                for (const element of kv[1]) {
                    if (allDeps.has(element)) {
                        allDeps.get(element).add(kv[0]);
                    } else {
                        allDeps.set(element, new Set([kv[0]]));
                    }
                }
            }

            for (const kv of scriptBundleDeps) {
                for (const element of kv[1]) {
                    if (allDeps.has(element)) {
                        allDeps.get(element).add(kv[0]);
                    } else {
                        allDeps.set(element, new Set([kv[0]]));
                    }
                }
            }

            let dirty = false;
            for (let i = autoExcludeBundles.length - 1; i >= 0; i--) {
                const exclude = autoExcludeBundles[i];
                if (!allDeps.has(exclude)) {
                    if (deps.has(exclude)) {
                        deps.delete(exclude);
                        dirty = true;
                    }
                    if (scriptBundleDeps.has(exclude)) {
                        scriptBundleDeps.delete(exclude);
                        dirty = true;
                    }
                }
            }

            if (!dirty) {
                for (let i = autoExcludeBundles.length - 1; i >= 0; i--) {
                    const exclude = autoExcludeBundles[i];
                    if (allDeps.has(exclude)) {
                        autoExcludeBundles.splice(i, 1);
                    }
                }

                break;
            }
        }
    }

    const preloadBundles = new Set<string>();
    settings.assets.preloadBundles.forEach((item) => {
        if (item.bundle) {
            preloadBundles.add(item.bundle);
        }
    });

    let data: any = {};
    for (const kv of deps) {
        const set = kv[1];
        if (set && set.size > 0) {
            let list = [];
            for (const d of set) {
                if (preloadBundles.has(d))
                    continue;
                list.push(d);
            }

            if (list.length <= 0)
                continue;

            list.sort((a: string, b: string) => {
                if (!deps.has(a)) return -1;
                if (!deps.has(b)) return 1;
                if (deps.get(b).has(a)) return -1;
                if (deps.get(a).has(b)) return 1;
                return 0;
            })

            data[kv[0]] = list;
        }
    }

    // 递归查找所有依赖并检查是否循环依赖
    let hasCircle = extendAndCheckCircleDependent(allBundles, deps);
    if (hasCircle) {
        throw new Error('存在循环依赖，请检查依赖关系！' + JSON.stringify(data));
    }

    settings.assets['dependencies'] = data;
    settings.assets['releaseActions'] = releaseActions;

    // 排除所有未被依赖且自动排除的Bundle
    for (const name of autoExcludeBundles) {
        excludeBundle(name, settings, dstPath);
    }

    fs.writeFileSync(file, JSON.stringify(settings));
}